package per.hnvcam.genetic.algorithm.impl;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import per.hnvcam.common.OrderedSet;
import per.hnvcam.common.impl.OrderedSetImpl;
import per.hnvcam.genetic.algorithm.Chromosome;
import per.hnvcam.genetic.algorithm.FitnessCalculator;
import per.hnvcam.genetic.algorithm.GeneticOperations;

/**
 * Created by IntelliJ IDEA.
 * User: Vi Cam
 * Date: 25/12/2011
 * Time: 20:54
 * To change this template use File | Settings | File Templates.
 */
@Service("GeneticOperations")
public class GeneticOperationsImpl implements GeneticOperations {

   private int divideDistance = 2;
   private double scalingDistance = 0.1;
   private int loopLimit;
   private Set<Chromosome> selectedChromosomes;

   @Autowired
   private FitnessCalculator fitnessCalculator;

   public GeneticOperationsImpl() {
      loopLimit = 0;
      selectedChromosomes = new HashSet<Chromosome>();
   }

   /**
    * Mixed between simple crossover and arithmetic crossover
    * @param a
    * @param b
    * @return
    */
   @Override
   public Chromosome[] applyCrossover(Chromosome a, Chromosome b) {
      int chromosomeLength = a.getGenes().size();
      int dividePosition = getDividePosition(chromosomeLength);
      double scaling  = getScaling();
      int middleFirst = (int) (scaling * dividePosition);
      int middleLast = dividePosition + (int) ( (1 - scaling) * (chromosomeLength - dividePosition) );
      List<Long> geneA = new ArrayList<Long>(chromosomeLength);
      List<Long> geneB = new ArrayList<Long>(chromosomeLength);
      for (int i = 0; i < chromosomeLength; i++) {
         if (i < middleFirst || (i >= dividePosition && i < middleLast)) {
            geneA.add(a.getGenes().get(i));
            geneB.add(b.getGenes().get(i));
         } else {
            geneA.add(b.getGenes().get(i));
            geneB.add(a.getGenes().get(i));
         }
      }
      return new Chromosome[] {new Chromosome(geneA), new Chromosome(geneB)};
   }

   private double getScaling() {
      double result = Math.random();
      while (result < scalingDistance || result > (1 - scalingDistance)) {
         result = Math.random();
      }
      return result;
   }

   private int getDividePosition(int size) {
      if (size <= divideDistance * 2) {
         return size / 2;
      }
      int result = 0;
      while (result < divideDistance || result > (size - divideDistance)) {
         result = (int) (size * Math.random());
      }
      return result;
   }

   @Override
   public Chromosome [] applySelection(List<Chromosome> population) {
      OrderedSet<Chromosome> result = sortPopulationByFitness(population);
      return new Chromosome[] {selectOneChromosome(result), selectOneChromosome(result)}; 
   }
   
   
   
   private Chromosome selectOneChromosome(OrderedSet<Chromosome> population) {
      int loopCount = 0;
      int maxLoopCount = loopLimit != 0 ? loopLimit : population.size();
      Chromosome result = population.getAt(0);
      while (selectedChromosomes.contains(result) && loopCount < (maxLoopCount - 1)) {
         loopCount++;
         result = population.getAt(loopCount);
      }
      selectedChromosomes.add(result);
      return result;
   }

   @Override
   public Chromosome applyMutation(Chromosome a, int position, Long newGene) {
      Chromosome b = new Chromosome();
      b.getGenes().addAll(a.getGenes());
      return b;
   }

   @Override
   public Chromosome[] applyReplacement(Chromosome[] parents, Chromosome [] children) {
      Chromosome[] result = new Chromosome[2];
      TreeMap<Double, Chromosome> orderedTree = new TreeMap<Double, Chromosome>();
      orderedTree.put(fitnessCalculator.getFitness(parents[0]), parents[0]);
      orderedTree.put(fitnessCalculator.getFitness(parents[1]), parents[1]);
      orderedTree.put(fitnessCalculator.getFitness(children[0]), children[0]);
      orderedTree.put(fitnessCalculator.getFitness(children[1]), children[1]);

      Map.Entry<Double, Chromosome> greatest = orderedTree.lastEntry();
      result[0] = greatest.getValue();
      result[1] = orderedTree.floorEntry(greatest.getKey()).getValue();

      return result;
   }

   public int getDivideDistance() {
      return divideDistance;
   }

   public void setDivideDistance(int divideDistance) {
      this.divideDistance = divideDistance;
   }

   public double getScalingDistance() {
      return scalingDistance;
   }

   public void setScalingDistance(double scalingDistance) {
      this.scalingDistance = scalingDistance;
   }

   public FitnessCalculator getFitnessCalculator() {
      return fitnessCalculator;
   }

   public void setFitnessCalculator(FitnessCalculator fitnessCalculator) {
      this.fitnessCalculator = fitnessCalculator;
   }

   private class ChromosomeComparator implements Comparator<Chromosome> {

      @Override
      public int compare(Chromosome o1, Chromosome o2) {
         return Double.valueOf(fitnessCalculator.getFitness(o1)).compareTo(fitnessCalculator.getFitness(o2));
      }
   }

   @Override
   public OrderedSet<Chromosome> sortPopulationByFitness(List<Chromosome> population) {
      OrderedSet<Chromosome> result = new OrderedSetImpl<Chromosome>(new ChromosomeComparator());
      result.addAll(population);
      return result;
   }
}
